﻿using AutoMapper.Internal;
using NetTopologySuite.Geometries;
using System;
using System.Collections.Generic;
using System.Linq;

namespace housingsafety.server.Geos
{
    /// <summary>
    /// 网格距离法
    /// 1、网格法，聚合出所要的点
    /// 2、直接距离法，进一步聚合
    /// 点聚合算法要先将经纬度转换为坐标
    /// 点聚合的聚合半径要设置合理，比如中国的长宽在5000公里级别，所以聚合半径最好大于1000米，小于100公里
    /// 点聚合算法，数据量大的时候，可能时间比较长，可以考虑将计算结果放到redis或者文件系统中，用离线计算的方式
    /// </summary>
    public class GridDistanceMethod
    {
        /// <summary>
        /// 经纬度转换为坐标
        /// </summary>
        /// <param name="poi">经纬度</param>
        /// <returns>墨卡托</returns>
        private Point ToMercator(Point poi)
        {
            //[114.32894, 30.585748]
            var mercator = new Point(new Coordinate());
            var earthRad = 6378137.0;
            // console.log("mercator-poi",poi);
            mercator.X = poi.X/*lng*/ * Math.PI / 180 * earthRad;
            var a = poi.Y/*lat*/ * Math.PI / 180;
            mercator.Y = earthRad / 2 * Math.Log((1.0 + Math.Sin(a)) / (1.0 - Math.Sin(a)));
            // console.log("mercator",mercator);
            return mercator; //[12727039.383734727, 3579066.6894065146]
        }

        private Envelope CalcCorners(IList<Point> mockData)
        {
            var minX = 0.0;
            var minY = 0.0;
            var maxX = 0.0;
            var maxY = 0.0;
            mockData.ForAll(item =>
            {
                var lonlat = ToMercator(item);
                maxX = Math.Max(maxX, lonlat.X);
                maxY = Math.Max(maxY, lonlat.Y);
                minX = Math.Min(minX, lonlat.X);
                minY = Math.Min(minY, lonlat.Y);
            });

            return new Envelope(minX, maxX, minY, maxY);
        }

        /// <summary>
        /// 墨卡托转经纬度
        /// </summary>
        /// <param name="poi">墨卡托</param>
        /// <returns>经纬度</returns>
        private Point ToLngLat(PointRectangle poi)
        {
            var lnglat = new Point(new Coordinate());
            lnglat.X = poi.X / 20037508.34 * 180;
            var mmy = poi.Y / 20037508.34 * 180;
            lnglat.Y = 180 / Math.PI * (2 * Math.Atan(Math.Exp(mmy * Math.PI / 180)) - Math.PI / 2);
            return lnglat;
        }

        /// <summary>
        /// 分割成多个小矩形
        /// </summary>
        /// <param name="sourceData"></param>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="radius"></param>
        /// <returns></returns>
        private IDictionary<RectangleIndex, PointRectangle> Split2Rectangles(IList<Point> sourceData
            , double minX, double minY, double radius)
        {
            var points = new Dictionary<RectangleIndex, PointRectangle>();
            sourceData.ForAll(item =>
            {
                var i = Math.Floor((item.X - minX) / radius);
                var j = Math.Floor((item.Y - minY) / radius);

                var xy = new RectangleIndex(i, j);
                if (!points.ContainsKey(xy))
                {
                    points.Add(xy, new PointRectangle() { GetPoints = new List<Point>() });
                }

                points[xy].GetPoints.Add(item);
            });

            foreach (var p in points)
            {
                p.Value.X = p.Value.GetPoints.Select(x => x.X).Sum() / p.Value.GetPoints.Count;

                p.Value.I = (p.Value.X % radius) > (0.5 * radius) ? 1 : -1;

                p.Value.Y = p.Value.GetPoints.Select(x => x.Y).Sum() / p.Value.GetPoints.Count;

                p.Value.J = (p.Value.Y % radius) > (0.5 * radius) ? 1 : -1;
            }

            return points;
        }

        private bool MathMerge(PointRectangle point1, PointRectangle point2, double radius)
        {
            if (null != point2 && !point2.IsMath && Math.Abs(point1.X - point2.X) < (0.5 * radius))
            {
                var tmpData = new List<Point>(point1.GetPoints);
                tmpData.AddRange(point2.GetPoints);

                var x = tmpData.Select(x => x.X).Sum() / tmpData.Count;
                var y = tmpData.Select(x => x.Y).Sum() / tmpData.Count;
                var i = (x % radius) > (0.5 * radius) ? 1 : -1;
                var j = (y % radius) > (0.5 * radius) ? 1 : -1;
                if (i == point1.I && j == point1.J)
                {
                    point2.IsMath = true;
                    point1.GetPoints = tmpData;
                    point1.X = x;
                    point1.Y = y;
                    point1.I = i;
                    point1.J = j;
                    return false;
                }
                else
                {
                    point2.GetPoints = tmpData;
                    point2.X = x;
                    point2.Y = y;
                    point2.I = i;
                    point2.J = j;
                    point1.IsMath = true;
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// 小矩形之间的聚合
        /// </summary>
        private List<PointMerged> MergeSplit(IDictionary<RectangleIndex, PointRectangle> sourceData, double radius)
        {
            foreach (var item in sourceData)
            {
                var tmpPoint = item.Value;

                if (!MathMerge(tmpPoint, sourceData[new RectangleIndex(item.Key.X + tmpPoint.I, item.Key.Y)], radius))
                {
                    if (!MathMerge(tmpPoint, sourceData[new RectangleIndex(item.Key.X, item.Key.Y + tmpPoint.J)], radius))
                    {
                        MathMerge(tmpPoint, sourceData[new RectangleIndex(item.Key.X + tmpPoint.I, item.Key.Y + tmpPoint.J)], radius);
                    }
                }
            }

            var resultData = new List<PointMerged>();
            foreach (var item in (sourceData))
            {
                if (!item.Value.IsMath)
                {
                    var lngLat = ToLngLat(item.Value);

                    resultData.Add(new PointMerged()
                    {
                        Count = item.Value.GetPoints.Count,
                        GeoPoint = new Point(lngLat.X, lngLat.Y)
                    });
                }
            }

            return resultData;
        }

        /// <summary>
        /// 聚合
        /// </summary>
        /// <param name="sourceData"></param>
        /// <param name="radius"></param>
        /// <returns></returns>
        public List<PointMerged> Merge(IList<Point> sourceData, double radius)
        {
            Envelope env = CalcCorners(sourceData);

            var sps = Split2Rectangles(sourceData, env.MinX, env.MinY, radius);

            return MergeSplit(sps, radius);
        }

        /// <summary>
        /// 聚合
        /// </summary>
        /// <param name="sourceData"></param>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="radius"></param>
        /// <returns></returns>
        public List<PointMerged> Merge(IList<Point> sourceData, double minX, double minY, double radius)
        {
            var sps = Split2Rectangles(sourceData, minX, minY, radius);
            return MergeSplit(sps, radius);
        }
    }

    public class PointMerged
    {
        public Point GeoPoint { get; set; }
        public long Count { get; set; }
    }

    public class PointRectangle
    {
        public IList<Point> GetPoints { get; set; }
        public double X { get; set; }
        public double Y { get; set; }
        public int I { get; set; }
        public int J { get; set; }
        public bool IsMath { get; set; }
    }

    public class RectangleIndex
    {
        public RectangleIndex(double x, double y)
        {
            X = (int)x;
            Y = (int)y;
            XY = $"{X}_{Y}";
        }

        public int X { get; set; }
        public int Y { get; set; }
        public string XY { get; set; }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }

            RectangleIndex rectangleIndex = obj as RectangleIndex;
            if (null != rectangleIndex)
            {
                return X == rectangleIndex.X && Y == rectangleIndex.Y;
            }

            return false;
        }

        public override int GetHashCode()
        {
            return XY.GetHashCode();
        }

        public override string ToString()
        {
            return XY;
        }
    }
}
